package mybatis.xml.reload;

import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Mybatis xml 重新加载
 */
@Component
public class MybatisXmlMapperScan implements DisposableBean, InitializingBean, ApplicationContextAware {

    private volatile ApplicationContext context;
    /**
     * mapper.xml 文件名、文件大小与最后修改时间的映射 用于判断是否有变化
     */
    private final HashMap<String, String> fileMapping = new HashMap();
    private volatile Scanner scanner;
    private ScheduledExecutorService service;

    /**
     * 当前的运行环境  等于 dev 时开启自动重新加载 xml
     */
    @Value("${spring.profiles.active}")
    private String profile;

    /**
     * mybatis 或者 mybatis-plus 配置的 mapper-locations
     * 可通过 @Value("${mybatis.mapper-locations}") 或 @Value("${mybatis-plus.mapper-locations}") 获取
     */
    public String mybatisMapperLocations;

    public void reloadMybatisMapper(String name, String xmlContent, boolean throwE) {
        try {
            scanner.reloadXML(name, xmlContent);
        } catch (Exception e) {
            e.printStackTrace();
            if (throwE) {
                throw new RuntimeException(e);
            }
        }
    }

    public void removeMybatisMapper(String name, String xmlContent) {
        try {
            scanner.remove(name, xmlContent, scanner.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void destroy() throws Exception {
        if (this.service != null) {
            this.service.shutdownNow();
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        try {
            scanner = new Scanner();
            if("dev".equals(profile)){
                //创建一个线程池 只有一个线程
                service = Executors.newScheduledThreadPool(1);
                //延迟五秒后执行 并且在开始执行后每5秒执行一次
                service.scheduleAtFixedRate(() -> {
                    try {
                        if (scanner.isChanged()) {
                            scanner.reloadXML();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }, 5, 5, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }


    class Scanner {
        private String[] basePackages;
        private ResourcePatternResolver rpr = new PathMatchingResourcePatternResolver();

        public Scanner() {
            basePackages = StringUtils.tokenizeToStringArray(MybatisXmlMapperScan.this.mybatisMapperLocations,
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
        }

        private void reloadXML(String name, String xmlContent) throws Exception {
            try {
                this.refresh(name, xmlContent);
            } catch (Exception e) {
                System.err.println("重新加载Mybatis>>>" + name + "出错了，赶紧看看....");
                throw new NestedIOException("Failed to parse mapping resource: '" + name + "'", e);
            } finally {
                ErrorContext.instance().reset();
            }
            System.out.println(name + " 加载完成");
        }

        private void reloadXML() throws Exception {
            SqlSessionFactory factory = MybatisXmlMapperScan.this.context.getBean(SqlSessionFactory.class);
            Configuration configuration = factory.getConfiguration();
            removeConfig(configuration);
            String p = Arrays.asList(this.basePackages).toString();
            System.out.println("开始重新加载Mybatis: " + p + " 所有的*Mapeper.xml 和*DAO.xml ");
            String[] mapperLocations = this.basePackages;
            for (String mapperLocation : mapperLocations) {
                Resource[] resources = this.getResource(mapperLocation);
                if (resources != null) {
                    for (int i = 0; i < resources.length; ++i) {
                        if (resources[i] != null) {
                            try {
                                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resources[i].getInputStream(), configuration, resources[i].toString(), configuration.getSqlFragments());
                                xmlMapperBuilder.parse();
                            } catch (Exception e) {
                                System.err.println("重新加载Mybatis>>>" + resources[i].getFile().getAbsolutePath() + "出错了，赶紧看看....");
                                throw new NestedIOException("Failed to parse mapping resource: '" + resources[i] + "'", e);
                            } finally {
                                ErrorContext.instance().reset();
                            }
                        }
                    }
                }
            }
            System.out.println("重新加载加载Mybatis: " + p + " 所有的*Mapeper.xml 和*DAO.xml 完成");
        }

        private Resource[] getResource(String basePackage) throws IOException {
            Resource[] resources = this.rpr.getResources(basePackage);
            return resources;
        }

        private void removeConfig(Configuration configuration) throws Exception {
            Class<?> classConfig = configuration.getClass();
            this.clearMap(classConfig, configuration, "mappedStatements");
            this.clearMap(classConfig, configuration, "caches");
            this.clearMap(classConfig, configuration, "resultMaps");
            this.clearMap(classConfig, configuration, "parameterMaps");
            this.clearMap(classConfig, configuration, "keyGenerators");
            this.clearMap(classConfig, configuration, "sqlFragments");
            this.clearSet(classConfig, configuration, "loadedResources");
        }

        private void clearMap(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
            Field field = classConfig.getDeclaredField(fieldName);
            field.setAccessible(true);
            Map mapConfig = (Map) field.get(configuration);
            mapConfig.clear();
        }

        private void clearSet(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
            Field field = classConfig.getDeclaredField(fieldName);
            field.setAccessible(true);
            Set setConfig = (Set) field.get(configuration);
            setConfig.clear();
        }

        public void scan() throws IOException {
            if (MybatisXmlMapperScan.this.fileMapping.isEmpty()) {
                String[] mapperLocations = this.basePackages;
                for (String mapperLocation : mapperLocations) {
                    Resource[] resources = this.getResource(mapperLocation);
                    if (resources != null) {
                        for (int i = 0; i < resources.length; ++i) {
                            String multi_key = this.getValue(resources[i]);
                            MybatisXmlMapperScan.this.fileMapping.put(resources[i].getFilename(), multi_key);
                        }
                    }
                }
            }
        }

        private String getValue(Resource resource) throws IOException {
            String contentLength = String.valueOf(resource.contentLength());
            String lastModified = String.valueOf(resource.lastModified());
            return contentLength + lastModified;
        }

        public boolean isChanged() throws IOException {
            boolean isChanged = false;
            String[] mapperLocations = this.basePackages;
            for (String mapperLocation : mapperLocations) {
                Resource[] resources = this.getResource(mapperLocation);
                if (resources != null) {
                    for (int i = 0; i < resources.length; ++i) {
                        String name = resources[i].getFilename();
                        String value = MybatisXmlMapperScan.this.fileMapping.get(name);
                        String multi_key = this.getValue(resources[i]);
                        if (!multi_key.equals(value)) {
                            isChanged = true;
                            MybatisXmlMapperScan.this.fileMapping.put(name, multi_key);
                        }
                    }
                }
            }
            return isChanged;
        }

        private void refresh(Resource resource) throws Exception {
            Configuration configuration = getConfiguration();
            if (configuration != null) {
                try {
                    Set<String> loadedResourcesSet = getLoadedResources(configuration);
                    XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(), new XMLMapperEntityResolver());
                    removeContext(configuration, xPathParser);
                    loadedResourcesSet.remove(resource.toString());
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(), configuration, resource.toString(), configuration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } catch (Exception e) {
                    throw new NestedIOException("Failed to parse mapping resource: '" + resource + "'", e);
                } finally {
                    ErrorContext.instance().reset();
                }
            }
        }

        private void refresh(String name, String xmlContent) throws Exception {
            Configuration configuration = getConfiguration();
            if (configuration != null) {
                try {
                    this.remove(name, xmlContent, configuration);
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(new ByteArrayInputStream(xmlContent.getBytes()), configuration, name, configuration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } catch (Exception e) {
                    throw new NestedIOException("Failed to parse mapping resource: '" + name + "'", e);
                } finally {
                    ErrorContext.instance().reset();
                }
            }
        }

        private void remove(String name, String xmlContent, Configuration configuration) throws NoSuchFieldException, IllegalAccessException {
            if (configuration != null) {
                Set<String> loadedResourcesSet = getLoadedResources(configuration);
                XPathParser xPathParser = new XPathParser(xmlContent, true, configuration.getVariables(), new XMLMapperEntityResolver());
                removeContext(configuration, xPathParser);
                loadedResourcesSet.remove(name);
            }
        }

        private void removeContext(Configuration configuration, XPathParser xPathParser) {
            XNode context = xPathParser.evalNode("/mapper");
            String namespace = context.getStringAttribute("namespace");
            try {
                Field field = MapperRegistry.class.getDeclaredField("knownMappers");
                field.setAccessible(true);
                Map mapConfig = (Map) field.get(configuration.getMapperRegistry());
                mapConfig.remove(Resources.classForName(namespace));
            } catch (Exception e) {
                e.printStackTrace();
            }
            configuration.getCacheNames().remove(namespace);
            this.cleanMappedStatements(context.evalNodes("select|delete|insert|update"), namespace, configuration);
            this.cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace, configuration);
            this.cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace, configuration);
            this.cleanKeyGenerators(context.evalNodes("insert|update"), namespace, configuration);
            this.cleanSqlElement(context.evalNodes("/mapper/sql"), namespace, configuration);
        }

        private Set<String> getLoadedResources(Configuration configuration) throws NoSuchFieldException, IllegalAccessException {
            if (configuration != null) {
                //判断用的是 mybatis 的 Configuration 还是 mybatis-plus 的 MybatisConfiguration
                boolean isSupper = configuration == null ? false : configuration.getClass().getSuperclass() == Configuration.class;
                Field loadedResourcesField = isSupper ? configuration.getClass().getSuperclass().getDeclaredField("loadedResources") : configuration.getClass().getDeclaredField("loadedResources");
                loadedResourcesField.setAccessible(true);
                //获取 configuration 中的 loadedResources
                return (Set<String>) loadedResourcesField.get(configuration);
            }
            return null;
        }

        private Configuration getConfiguration() {
            SqlSessionFactory factory = MybatisXmlMapperScan.this.context.getBean(SqlSessionFactory.class);
            return factory.getConfiguration();
        }

        private void cleanParameterMap(List<XNode> list, String namespace, Configuration configuration) {
            for (XNode xNode : list) {
                String id = xNode.getStringAttribute("id");
                configuration.getParameterMaps().remove(namespace + "." + id);
            }
        }

        private void cleanResultMap(List<XNode> list, String namespace, Configuration configuration) {
            for (XNode xNode : list) {
                String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier());
                configuration.getResultMapNames().remove(id);
                configuration.getResultMapNames().remove(namespace + "." + id);
            }
        }

        private void cleanKeyGenerators(List<XNode> list, String namespace, Configuration configuration) {
            for (XNode xNode : list) {
                String id = xNode.getStringAttribute("id");
                configuration.getKeyGeneratorNames().remove(id + "!selectKey");
                configuration.getKeyGeneratorNames().remove(namespace + "." + id + "!selectKey");
            }
        }

        private void cleanSqlElement(List<XNode> list, String namespace, Configuration configuration) {
            for (XNode xNode : list) {
                String id = xNode.getStringAttribute("id");
                configuration.getSqlFragments().remove(id);
                configuration.getSqlFragments().remove(namespace + "." + id);
            }
        }

        private void cleanMappedStatements(List<XNode> list, String namespace, Configuration configuration) {
            for (XNode xNode : list) {
                String id = xNode.getStringAttribute("id");
                configuration.getMappedStatementNames().remove(id);
                configuration.getMappedStatementNames().remove(namespace + "." + id);
            }
        }

        private List<String> getNodeIds(String xmlContent) {
            Configuration configuration = getConfiguration();
            List<String> nodeIds = new ArrayList();
            if (configuration != null) {
                try {
                    XPathParser xPathParser = new XPathParser(xmlContent, true, configuration.getVariables(), new XMLMapperEntityResolver());
                    XNode context = xPathParser.evalNode("/mapper");
                    List<XNode> evalNodes = context.evalNodes("select|delete|insert|update");
                    for (XNode node : evalNodes) {
                        String id = node.getStringAttribute("id");
                        nodeIds.add(id);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    ErrorContext.instance().reset();
                }
            }
            return nodeIds;
        }
    }
}
